/*
 * Copyright 2009-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License i distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.paoding.rose.jade.statement;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import net.paoding.rose.jade.annotation.IngoreField;
import net.paoding.rose.jade.annotation.Insert;
import net.paoding.rose.jade.annotation.Paged;
import net.paoding.rose.jade.annotation.SQL;
import net.paoding.rose.jade.annotation.SQLParam;
import net.paoding.rose.jade.annotation.SQLType;
import net.paoding.rose.jade.annotation.ShardBy;
import net.paoding.rose.jade.annotation.Update;
import net.paoding.rose.jade.context.FieldParameter;
import net.paoding.rose.jade.dataaccess.DataAccess;
import net.paoding.rose.jade.dataaccess.DataAccessFactory;
import net.paoding.rose.jade.rowmapper.Pagination;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MethodInvoker;

/**
 * {@link StatementMetaData} 封装、缓存了一个DAO方法的相关信息
 * <p>
 * 
 * 
 * @author 王志亮 [qieqie.wang@gmail.com]
 */
public class StatementMetaData {
	
	protected static Log logger = LogFactory.getLog(StatementMetaData.class);

    /**
     * 所属的DAO类的classMetaData
     */
    private final DAOMetaData daoMetaData;

    /**
     * 所在的DAO方法
     */
    private final Method method;

    /**
     * DAO方法上的原始SQL语句
     */
    private String sql;

    /**
     * 方法返回参数的范型类型（不支持多级）－从method中获取并缓存
     */
    private final Class<?>[] genericReturnTypes;

    /**
     * {@link SQLParam} 注解数组－从method中获取并缓存
     * <p>
     * 此数组的长度为方法的参数个数，如果对应位置的方法参数没有注解 {@link SQLParam},该位置的元素值为null
     */
    private final SQLParam[] sqlParams;

    /**
     * <code>@{@link ShardBy}</code>标注在哪个参数上？(从0开始，负数代表无)－从method中获取并缓存
     */
    private final int shardByIndex;

    private final ShardBy shardBy;

    private final int parameterCount;

    private DataAccessFactory dataAccessFactory;
    // --------------------------------------------

    public StatementMetaData(DAOMetaData daoMetaData, Method method, Object[] args, DataAccessFactory dataAccessFactory) {
        this.daoMetaData = daoMetaData;
        this.method = method;
        this.dataAccessFactory = dataAccessFactory;
        
        if(method.isAnnotationPresent(Insert.class)){
        	this.sql = this.generateSqlForInsert(method, method.getAnnotation(Insert.class), args);
        } else if(method.isAnnotationPresent(Update.class)){
        	this.sql = this.generateSqlForUpdate(method, method.getAnnotation(Update.class), args);
        } else if(method.isAnnotationPresent(Paged.class)){
        	this.sql = this.generatePagerSql(method, method.getAnnotation(Paged.class), args);
        } else {
        	this.sql = method.getAnnotation(SQL.class).value();
        }

        this.genericReturnTypes = GenericUtils.getActualClass(method.getGenericReturnType());

        Annotation[][] annotations = method.getParameterAnnotations();
        this.parameterCount = annotations.length;
        this.sqlParams = new SQLParam[annotations.length];
        int shardByIndex = -1;
        ShardBy shardBy = null;
        for (int index = 0; index < annotations.length; index++) {
            for (Annotation annotation : annotations[index]) {
                if (annotation instanceof ShardBy) {
                    if (shardByIndex >= 0) {
                        throw new IllegalArgumentException("duplicated @" + ShardBy.class.getName());
                    }
                    shardByIndex = index;
                    shardBy = (ShardBy) annotation;
                } else if (annotation instanceof SQLParam) {
                    this.sqlParams[index] = (SQLParam) annotation;
                }
            }
        }
        this.shardByIndex = shardByIndex;
        this.shardBy = shardBy;
    }

    public DAOMetaData getDAOMetaData() {
        return daoMetaData;
    }

    public Method getMethod() {
        return method;
    }

    public String getSQL() {
        return sql;
    }

    public int getParameterCount() {
        return parameterCount;
    }

    public SQLParam getSQLParamAt(int argIndex) {
        return sqlParams[argIndex];
    }

    public int getShardByIndex() {
        return shardByIndex;
    }

    public ShardBy getShardBy() {
        return shardBy;
    }

    public Class<?>[] getGenericReturnTypes() {
        return genericReturnTypes;
    }

    public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
        return method.getAnnotation(annotationClass);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof StatementMetaData) {
            StatementMetaData modifier = (StatementMetaData) obj;
            return daoMetaData.equals(modifier.daoMetaData) && method.equals(modifier.method);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return daoMetaData.hashCode() ^ method.hashCode();
    }

    @Override
    public String toString() {
        return daoMetaData.getDAOClass().getName() + '#' + method.getName();
    }

    private static Pattern[] SELECT_PATTERNS = new Pattern[] {
            Pattern.compile("^\\s*SELECT\\s+", Pattern.CASE_INSENSITIVE), //
            Pattern.compile("^\\s*SHOW\\s+", Pattern.CASE_INSENSITIVE), //
            Pattern.compile("^\\s*DESC\\s+", Pattern.CASE_INSENSITIVE), //
            Pattern.compile("^\\s*DESCRIBE\\s+", Pattern.CASE_INSENSITIVE), //
    };

    private SQLType sqlType;

    public SQLType getSQLType() {
        if (sqlType == null) {
        	if(method.isAnnotationPresent(Insert.class) || method.isAnnotationPresent(Update.class)){
        		this.sqlType = SQLType.WRITE;
        	} else {
        		SQL sql = method.getAnnotation(SQL.class);
                SQLType sqlType = sql.type();
                if (sqlType == SQLType.AUTO_DETECT) {
                    for (int i = 0; i < SELECT_PATTERNS.length; i++) {
                        // 用正则表达式匹配  SELECT 语句
                        if (SELECT_PATTERNS[i].matcher(getSQL()).find()) {
                            sqlType = SQLType.READ;
                            break;
                        }
                    }
                    if (sqlType == SQLType.AUTO_DETECT) {
                        sqlType = SQLType.WRITE;
                    }
                }
                this.sqlType = sqlType;
        	}
        }
        return sqlType;
    }
    
    private String generateSqlForInsert(Method method, Insert insert, Object[] args){
    	StringBuffer sqlBuffer = new StringBuffer();
    	if(null == args || 0 == args.length || ClassUtils.isPrimitiveOrWrapper(args[0].getClass())){
    		return sqlBuffer.toString();
    	}
    	
    	Object instance = args[0];
    	if(List.class.isAssignableFrom(args[0].getClass())){
    		List<?> dataList = (List<?>)args[0];
    		if(null != dataList && !dataList.isEmpty()){
    			instance = dataList.get(0);
    		}
    	}
    	List<FieldParameter> paramList = getFieldParameters(instance, false);
    	List<String> columnNameList = this.getColumnNameList(insert.tableName());
    	
    	sqlBuffer.append("insert into ").append(insert.tableName()).append("(");
    	StringBuffer paramBuffer = new StringBuffer();
    	for(FieldParameter param: paramList){
    		if(!columnNameList.contains(param.getColumnName())){
				continue;
			}
    		if(0 < paramBuffer.length()){
    			sqlBuffer.append(",");
				paramBuffer.append(",");
			}
    		sqlBuffer.append(param.getColumnName());
			paramBuffer.append(":1.").append(param.getName());
    	}
    	sqlBuffer.append(") values(").append(paramBuffer).append(")");
    	return sqlBuffer.toString();
    }
    
    private String generateSqlForUpdate(Method method, Update update, Object[] args){
    	StringBuffer sqlBuffer = new StringBuffer();
    	if(null == args || 0 == args.length || ClassUtils.isPrimitiveOrWrapper(args[0].getClass())){
    		return sqlBuffer.toString();
    	}
    	
    	Object instance = args[0];
    	if(List.class.isAssignableFrom(args[0].getClass())){
    		List<?> dataList = (List<?>)args[0];
    		if(null != dataList && !dataList.isEmpty()){
    			instance = dataList.get(0);
    		}
    	}
    	List<FieldParameter> paramList = getFieldParameters(instance, false);
    	List<String> columnNameList = this.getColumnNameList(update.tableName());
    	
    	sqlBuffer.append("update ").append(update.tableName()).append(" set ");
    	for(FieldParameter param: paramList){
    		if(!columnNameList.contains(param.getColumnName())){
				continue;
			}
    		if(update.keyColumn().equals(param.getName())){
    			continue;
    		}
    		if(!sqlBuffer.toString().endsWith(" ")){
    			sqlBuffer.append(",");
			}
    		sqlBuffer.append(param.getColumnName()).append("=:1.").append(param.getName());
    	}
    	sqlBuffer.append(" where ").append(FieldParameter.convertFieldNameToColumnName(update.keyColumn()))
    		.append("=:1.").append(update.keyColumn());
    	return sqlBuffer.toString();
    }
    
    private String generatePagerSql(Method method, Paged paged, Object[] args){
    	DataAccess dataAccess = this.dataAccessFactory.getDataAccess(this, null);
    	String databaseProductName = GenericUtils.getDatabasePruductName(dataAccess);
    	String sql = method.getAnnotation(SQL.class).value();
    	int paramIndex = paged.paramIndex();
    	Pagination pagination = new Pagination();
    	BeanUtils.copyProperties(args[paramIndex], pagination);
    	return getPagerSql(sql, pagination, paramIndex+1, databaseProductName);
    }
    
    /**
	 * 获取分页查询SQL
	 * @param sql
	 * @param totalSize
	 * @param currentPage start with 1
	 * @param pageSize 一页显示的记录数
	 * @param databaseProductName 数据库产品名称 MySQL, Oracle, DB2
	 * @return
	 */
	public static String getPagerSql(String sql, Pagination pagination, int paramIndex, String databaseProductName){
		StringBuffer buffer = new StringBuffer();
		if(GenericUtils.DB_TYPE_ORACLE.equals(databaseProductName)){
			buffer.append("select * from (select mt.*,rownum rn from (")
			.append(sql).append(" ) mt) where rn between :").append(paramIndex)
			.append(".startIndex+1 and :").append(paramIndex).append(".endIndex ");
		}else if(GenericUtils.DB_TYPE_DB2.equals(databaseProductName)){
			buffer.append("select * from (select ma.*,rownumber() over() as rn from(")
			.append(sql).append(" ) ma ) mt where rn between :").append(paramIndex)
			.append(".startIndex+1 and :").append(paramIndex).append(".endIndex ");
		}else if(GenericUtils.DB_TYPE_MYSQL.equals(databaseProductName)){
			buffer.append("select * from (").append(sql).append(") mt limit :")
			.append(paramIndex).append(".startIndex, :").append(paramIndex).append(".pageSize ");
		}
		return buffer.toString();
	}
    
    private List<String> getColumnNameList(String tableName) {
    	List<String> retList = new ArrayList<String>();
    	DataAccess dataAccess = this.dataAccessFactory.getDataAccess(this, null);
    	Connection conn = null;
    	try{
    		conn = dataAccess.getDataSource().getConnection();
    		String sql = "select * from " + tableName + " where 1=2 ";
    		PreparedStatement pstmt = conn.prepareStatement(sql);
    		ResultSet resultSet = pstmt.executeQuery();
			ResultSetMetaData rsmd = resultSet.getMetaData();
			int columnCount = rsmd.getColumnCount();
			for (int i = 1; i <= columnCount; ++i) {
				retList.add(JdbcUtils.lookupColumnName(rsmd, i).toLowerCase());
			}
    	} catch(Exception e){
    		logger.error("getColumnList error:", e);
    	} finally{
    		try {
				if(null != conn){
					conn.close();
					conn = null;
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
    	}
    	return retList;
    }
    
    @SuppressWarnings("rawtypes")
	public static List<FieldParameter> getFieldParameters(Object obj, boolean obtainValue){
		Assert.notNull(obj, "Can't get field values from a null object.");
		List<String> paramNameList = new ArrayList<String>();
		List<FieldParameter> retList = new ArrayList<FieldParameter>();
		Class clazz = obj.getClass();
		List<Field> fields = new ArrayList<Field>();
		getReflectField(clazz, fields);
		
		StringBuffer methodName = new StringBuffer();
		MethodInvoker invoker = new MethodInvoker();
		invoker.setTargetObject(obj);
		for(int index=0,len=fields.size(); index < len; index ++){
			Field field = fields.get(index);
			if(field.isAnnotationPresent(IngoreField.class)){
				continue;
			}
			int mod = field.getModifiers();
			if(!Modifier.isStatic(mod) && (Modifier.isPrivate(mod) || Modifier.isProtected(mod))){
				FieldParameter param = new FieldParameter();
				String fieldName = field.getName();
				param.setName(fieldName);
				param.setColumnName(FieldParameter.convertFieldNameToColumnName(fieldName));
				param.setFiledType(field.getType());
				Object value = null;
				try {
					if(obtainValue){
						methodName.setLength(0);
						methodName.append("get").append(fieldName.substring(0,1).toUpperCase())
							.append(fieldName.substring(1));
						invoker.setTargetMethod(methodName.toString());
						invoker.setArguments(new Object[]{});
						invoker.prepare();
						value = invoker.invoke();
					}
				} catch (Exception e) {
					logger.error("Error when fetch parameter [" + fieldName + "] from [" + clazz.getName() + "].");
				} finally{
					param.setValue(value);
					if(!paramNameList.contains(param.getName())){
						paramNameList.add(param.getName());
						retList.add(param);
					}
				}
			}
		}
		return retList;
	}
    
    @SuppressWarnings("rawtypes")
	private static void getReflectField(Class clazz, List<Field> fieldList){
		if(clazz == Object.class){
			return;
		}
		Field[] fields = clazz.getDeclaredFields();
		for(int index=0; index < fields.length; index ++){
			fieldList.add(fields[index]);
		}
		getReflectField(clazz.getSuperclass(), fieldList);
	}
}

